package org.dreamwork.assistants.iot.modbus.tcp;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.dreamwork.concurrent.Looper;
import org.dreamwork.modbus.ModbusException;
import org.dreamwork.modbus.Predefined;
import org.dreamwork.modbus.tcp.Client;
import org.dreamwork.modbus.tcp.IClientListener;
import org.dreamwork.util.IOUtil;
import org.dreamwork.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;

public class AppMain extends Application implements Initializable, IClientListener {
    public TextField txtServer;
    public CheckBox recvShowTime;
    public CheckBox recvHex;
    public CheckBox sendHex;
    public CheckBox autoSend;
    public HBox boxAutoSending;
    public WebView web;
    public ComboBox<Action> cbbActions;
    public ComboBox<Target> cbbTargets;
    public TextField txtAddress;
    public TextField txtCount;
    public TextArea txtContent;
    public TextField txtSendInterval;
    public Button btnSend;
    public Button btnConnect;
    public Button btnClearReceived;
    public HBox root;
    public TitledPane sendPanel;

    private final Logger logger = LoggerFactory.getLogger (AppMain.class);
    private final static String LOOP_NAME = "io";
    private final AtomicBoolean connected = new AtomicBoolean (false);
    private final SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss");
    private Client client;

    public static void main (String[] args) {
        launch (args);
    }

    @Override
    public void start (Stage stage) throws IOException {
        ClassLoader loader = AppMain.class.getClassLoader ();
        URL page = loader.getResource ("fxml/main.fxml");
        FXMLLoader fxml = new FXMLLoader (page);

        Parent parent = fxml.load ();
        Scene scene = new Scene (parent);
        stage.setScene (scene);
        stage.setTitle ("MODBUS-TCP调试助手");
        stage.setOnCloseRequest (event -> {
            if (client != null) {
                client.disconnect ();
            }
            Looper.exit ();
        });

        URL icon = loader.getResource ("images/modbus.png");
        if (icon != null) {
            try (InputStream in = icon.openStream ()) {
                stage.getIcons ().add (new Image (in));
            }
        }
        stage.show ();
    }

    @Override
    public void initialize (URL location, ResourceBundle resources) {
        cbbActions.getItems ().addAll (Action.values ());
        cbbTargets.getItems ().addAll (Target.values ());

        URL url = getClass ().getClassLoader ().getResource ("html/log.html");
        if (url != null) {
            try (InputStream in = url.openStream()) {
                byte[] buff = IOUtil.read(in);
                web.setContextMenuEnabled(false);
                web.getEngine().setJavaScriptEnabled(true);
                web.getEngine().loadContent(new String(buff, StandardCharsets.UTF_8));
            } catch (IOException ex) {
                logger.warn(ex.getMessage(), ex);
            }
        }

        Looper.create (LOOP_NAME, 16, 1);
    }

    public void changeConnectState () {
        if (connected.get ()) {
            if (client != null) {
                client.disconnect ();
            }
        } else {
            Looper.runInLoop (LOOP_NAME, this::connect);
        }
    }

    private void connect () {
        String host = txtServer.getText ();
        if (StringUtil.isEmpty (host)) {
            DialogHelp.error ("请输入服务器地址");
            return;
        }

        int port = 502;
        host = host.trim ();
        if (host.indexOf (':') > 0) {
            String[] a = host.split (":");
            host = a[0];
            try {
                port = Integer.parseInt (a[1]);
            } catch (NumberFormatException nfe) {
                DialogHelp.error ("无效的端口号");
                return;
            }
        }

        if (port <= 0 || port > 65535) {
            DialogHelp.error ("无效的端口号");
            return;
        }

        client = new Client (host, port)
                .setReuseAddress (true)
                .setSoTimeout (30000)
                .setListener (this)
                .setAutoReconnect (true);
        try {
            client.connect ();
        } catch (IOException e) {
            DialogHelp.error ("连接失败", e);
        }
    }

    public void changeAutoSendStatus () {
        boxAutoSending.setDisable (!autoSend.isSelected ());
    }

    public void onSend () {
        final Action action = cbbActions.getValue ();
        if (action == null) {
            DialogHelp.error ("请选择动作");
            return;
        }
        final Target target = cbbTargets.getValue ();
        if (target == null) {
            DialogHelp.error ("请选择目标");
            return;
        }

        String text = txtAddress.getText ();
        if (StringUtil.isEmpty (text)) {
            DialogHelp.error ("请填写地址");
            return;
        }
        final int address;
        try {
            address = Integer.parseInt (text.trim ());
            if (address < 0 || address > 0xffff) {
                DialogHelp.error ("无效的地址");
                return;
            }
        } catch (NumberFormatException nef) {
            DialogHelp.error ("无效的地址");
            return;
        }

        int count = 1;
        text = txtCount.getText ();
        String message = null;
        try {
            count = Integer.parseInt (text.trim ());
            if (count <= 0) {
                message = "无效的数量";
            }
        } catch (NumberFormatException nfe) {
            message = "无效的数量";
        }

        if (action == Action.MULTIPLE_WRITE) {
            if (StringUtil.isEmpty (text)) {
                DialogHelp.error ("请填写数量");
                return;
            }

            if (message != null) {
                DialogHelp.error (message);
                return;
            }
        } else {
            if (message != null)
                count = 1;
        }

        final Object data;
        String content = txtContent.getText ();
        if (action == Action.WRITE || action == Action.MULTIPLE_WRITE) {
            if (StringUtil.isEmpty (content)) {
                DialogHelp.error ("请输入要发送的数据");
                return;
            }
        }

        if (action == Action.WRITE) {
            if (target == Target.COILS) {
                try {
                    data = toBoolean (content.trim ());
                } catch (RuntimeException ex) {
                    DialogHelp.error (ex.getMessage ());
                    return;
                }
            } else if (target == Target.HOLDING_REGISTER) {
                try {
                    data = Integer.parseInt (content.trim ());
                } catch (NumberFormatException nfe) {
                    DialogHelp.error ("请输入整数");
                    return;
                }
            } else {
                DialogHelp.error ("不能" + action + target);
                return;
            }
        } else if (action == Action.MULTIPLE_WRITE) {
            String[] a = content.trim ().split (",");
            for (int i = 0; i < a.length; i ++) {
                String p = a[i];
                if (StringUtil.isEmpty (p)) {
                    DialogHelp.error ("无效的数组格式");
                    return;
                }
                a[i] = p.trim ();
            }

            if (target == Target.COILS) {
                boolean[] values = new boolean[a.length];
                try {
                    for (int i = 0; i < a.length; i++) {
                        values[i] = toBoolean (a[i]);
                    }
                    data = values;
                } catch (RuntimeException ex) {
                    DialogHelp.error (ex.getMessage ());
                    return;
                }
            } else if (target == Target.HOLDING_REGISTER) {
                int[] values = new int[a.length];
                try {
                    for (int i = 0; i < a.length; i ++) {
                        values[i] = Integer.parseInt (a[i]);
                    }
                    data = values;
                } catch (NumberFormatException nfe) {
                    DialogHelp.error ("请输入整数");
                    return;
                }
            } else {
                DialogHelp.error ("不能" + action + target);
                return;
            }
        } else {
            data = null;
        }

        final int size = count;
        Looper.runInLoop (LOOP_NAME, () -> {
            try {
                sendData (action, target, address, size, data);
            } catch (IOException ioe) {
                appendLog ("error", ioe.getMessage ());
                logger.warn (ioe.getMessage (), ioe);
            } catch (Exception ex) {
                ex.printStackTrace ();
            }
        });
    }

    private void sendData (Action action, Target target, int address, int count, Object data) throws IOException, ModbusException {
        int function = determineFunctionCode (action, target);
        switch (function) {
            case Predefined.Function.Read_Coils:
                performResult (address, count, client.readCoils (address, count));
                break;
            case Predefined.Function.Read_Discrete_Inputs:
                performResult (address, count, client.readDiscreteInputs (address, count));
                break;
            case Predefined.Function.Read_Holding_Registers:
                performResult (address, count, client.readHoldingRegisters (address, count));
                break;
            case Predefined.Function.Read_Input_Registers:
                performResult (address, count, client.readInputRegisters (address, count));
                break;
            case Predefined.Function.Write_Single_Coil:
                client.writeSingleCoil (address, (Boolean) data);
                break;
            case Predefined.Function.Write_Single_Register:
                client.writeSingleRegister (address, (Integer) data);
                break;
            case Predefined.Function.Write_Multiple_Coils:
                client.writeMultipleCoils (address, (boolean[]) data);
                break;
            case Predefined.Function.Write_Multiple_Registers:
                client.writeMultipleRegisters (address, (int[]) data);
                break;
            default:
                throw new RuntimeException ("Unsupported function: " + function);
        }
    }

    private int determineFunctionCode (Action action, Target target) {
        switch (action) {
            case READ:
                switch (target) {
                    case COILS:
                        return Predefined.Function.Read_Coils;
                    case DISCRETE_INPUT:
                        return Predefined.Function.Read_Discrete_Inputs;
                    case INPUT_REGISTER:
                        return Predefined.Function.Read_Input_Registers;
                    case HOLDING_REGISTER:
                        return Predefined.Function.Read_Holding_Registers;
                    default:
                        throw new RuntimeException ("action " + action + " can't apply to " + target);
                }
            case WRITE:
                switch (target) {
                    case COILS:
                        return Predefined.Function.Write_Single_Coil;
                    case HOLDING_REGISTER:
                    case INPUT_REGISTER:
                        return Predefined.Function.Write_Single_Register;
                    default:
                        throw new RuntimeException ("action " + action + " can't apply to " + target);
                }
            case MULTIPLE_WRITE:
                switch (target) {
                    case COILS:
                        return Predefined.Function.Write_Multiple_Coils;
                    case HOLDING_REGISTER:
                    case INPUT_REGISTER:
                        return Predefined.Function.Write_Multiple_Registers;
                    default:
                        throw new RuntimeException ("action " + action + " can't apply to " + target);
                }
            default:
                throw new RuntimeException ("unknown action: " + action);
        }
    }

    private boolean toBoolean (String content) {
        content = content.toLowerCase ();
        switch (content) {
            case "yes":
            case "y":
            case "true":
            case "t":
            case "1":
                return true;
            case "no":
            case "n":
            case "false":
            case "f":
            case "0":
                return false;
            default:
                throw new RuntimeException ("请输入逻辑值： yes/no|y/n|true/false|t/f|1/0");
        }
    }

    private void performResult (int address, int count, boolean[] values) {
        if (count == 1) {
            performResult (address, count, String.valueOf (values[0]));
        } else {
            performResult (address, count, Arrays.toString (values));
        }
    }

    private void performResult (int address, int count, int[] values) {
        if (count == 1) {
            performResult (address, count, String.valueOf (values[0]));
        } else {
            performResult (address, count, Arrays.toString (values));
        }
    }

    private void performResult (int address, int count, String content) {
        String message = String.format ("地址: %s, 数量: %d，服务器应答: %s", address, count, content);
        System.out.println (message);
        appendLog ("response", message);
    }

    private void error (String message) {
        appendLog ("error", message);
    }

    private void appendLog(String type, String message) {
        String timestamp = sdf.format(System.currentTimeMillis());
        String function = "appendLog ('" + timestamp + "', '" + type + "', '" + type + "', '" + message + "')";
        if (Platform.isFxApplicationThread()) {
            web.getEngine().executeScript(function);
        } else {
            Platform.runLater(() -> web.getEngine().executeScript(function));
        }
    }

    public void clearReceived () {
        String function = "clearLog ()";
        if (Platform.isFxApplicationThread()) {
            web.getEngine().executeScript(function);
        } else {
            Platform.runLater(() -> web.getEngine().executeScript(function));
        }
    }

    @Override
    public void onConnected (String host, int port) {
        Platform.runLater (() -> {
            btnConnect.setText ("断开连接");
            sendPanel.setDisable (false);
            connected.set (true);
        });
    }

    @Override
    public void onDisconnected (String host, int port) {
        Platform.runLater (() -> {
            btnConnect.setText ("连接");
            sendPanel.setDisable (true);
            connected.set (false);
        });
    }

    @Override
    public void onError (String host, int port, ModbusException cause) {
    }
}